/******************************************************************************************
Copyright(C), 2019-2021, 个人。
文件名：main.c
作者：Wind    版本：V1.0    日期：2020.4.1
文件描述：
    对纯视频H264码流的RTP传输的实现。
其它说明：
    当前文件仅用于个人学习。
历史修改记录：
1. 2020-4-1:Wind
创建。

2. 2021-6-19:Wind
修改格式以向实验室开放。
******************************************************************************************/
//+------------------------------------------------------------------------------------------+
//|                                       RTP测试说明
//+------------------------------------------------------------------------------------------+
//|  测试使用VLC播放器进行，首先建立文件输入以下内容并保存为.sdp文件：
//|  m=video 1118 RTP/AVP 96
//|  a=rtpmap:96 H264
//|  a=framerate:30
//|  c=IN IP4 169.254.134.37
//|  其中端口和RTP发送端地址需要根据实际设置修改，使用VLC打开此.sdp文件即可进行播放。
//|  注：如果VLC播放器提示SDP文件格式不正确，可更换VLC版本再试。
//|  注：播放前可能需首先关闭防火墙。
//+------------------------------------------------------------------------------------------+
//|                                         头文件包含
//+------------------------------------------------------------------------------------------+
/*|*/#include <stdio.h>
/*|*/#include <string.h>
/*|*/#include <stdlib.h>
/*|*/#include <unistd.h>
/*|*/#include <sys/ioctl.h>
/*|*/
/*|*/#include "L_RTP.h"
//+------------------------------------------------------------------------------------------+
//|  函数名称：L_RTP_CreateSession
//|  功能描述：创建一个RTP会话，主要是打开对客户端的Socket端口。
//|  参数说明：L_STRUCT_RTPSESSION结构体指针
//|  返回值说明：成功返回0，失败返回-1。
//|  备注：初始化中，执行如下操作：
//|         1. 检测结构体是否已经初始化过
//|         2. 打开一个数据报套接字端口
//|         3. 按需设置socket属性
//|         4. 获取本机IP
//|         5. 置结构体内已初始化标志位
//|        以上任意一步失败则回滚操作并退出。
//+------------------------------------------------------------------------------------------+
int L_RTP_CreateSession(L_STRUCT_RTPSESSION* RTPSession)
{
    //****************************************
    //这里的判断寄期望于编译器初始化结构体的时候
    //将此变量赋值为0，或者调用函数在设置参数时将
    //此标志位置0。
    //****************************************
    if(RTPSession->flag_inited)
    {
        printf("The Struct has been inited!\n");
        goto QUIT;
    }

    //对于没有初始化的结构体，某些变量需要初始化
    RTPSession->s32Sock     = -1;      //描述符无效
    RTPSession->pData       = NULL;    //数据指针为空
    RTPSession->DataLength  = 0;       //数据长度为0
    RTPSession->DataType    = RTP_NONE;//荷载类型无效
    RTPSession->SequenceNum = 0;      //RTP包序列号

    //****************************************
    //关于Unix中套接字使用的协议族，创建套接字时
    //采用PF_x，设置套接字时采用AF_x，这两种形式
    //的差别并不大，甚至可以混用。
    //****************************************
    if((RTPSession->s32Sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0)
    {
        printf("Open socket failed!\n");
        goto QUIT;
    }

    //如果地址为255.x.x.x则设置套接字允许广播
    if(0xFF000000 == (RTPSession->u32DestIP&0xFF000000))
    {
        int s32Broadcast = 1;
        if(-1 == setsockopt(RTPSession->s32Sock, SOL_SOCKET, SO_BROADCAST, (char *)&s32Broadcast, sizeof(s32Broadcast)))
        {
            printf("Set socket failed!\n");
            goto QUIT;
        }
    }

    //填充目的端地址
    RTPSession->stDestAddr.sin_family      = AF_INET;
    RTPSession->stDestAddr.sin_port        = htons(RTPSession->DestPort);
    RTPSession->stDestAddr.sin_addr.s_addr = RTPSession->u32DestIP;
    bzero(&(RTPSession->stDestAddr.sin_zero), 8);

    //****************************************
    //获取本机网络设备名
    //此处只尝试获取了eth0（有线网络）和wlan0
    //（无线网络），但是并非所有标号都是这两种，
    //在多网卡情况下或者使用特殊的网卡，默认名称
    //会随之更换，程序中也应作出修改。
    //****************************************
    strcpy(RTPSession->stIfreq.ifr_name, "eth0");
    if(ioctl(RTPSession->s32Sock, SIOCGIFADDR, &(RTPSession->stIfreq)) < 0)
    {
        printf("Get eth0 IP failed!\n");
        strcpy(RTPSession->stIfreq.ifr_name, "wlan0");
        if(ioctl(RTPSession->s32Sock, SIOCGIFADDR, &(RTPSession->stIfreq)) < 0)
        {
            printf("Get wlan0 IP failed!\n");
            goto QUIT;
        }
    }

    //****************************************
    //计算本机IP的网络字节序并保存
    //RTP是一个单向的发送协议，因此对于发送端而言
    //不需要指定源端口号，如果需要设置可以在此处
    //初始化的时候使用bind函数对Socket进行定位。
    //在客户端接收数据的时候也不需要发送端的端口号，
    //客户端监听的是目的端口号。
    //****************************************
    RTPSession->u32SrcIP = htonl(((struct sockaddr_in *)(&RTPSession->stIfreq.ifr_addr))->sin_addr.s_addr);

    RTPSession->flag_inited = 1;//置标志位，表示该结构指代的RTP会话已经初始化成功
    return 0;

QUIT:
    //Socket描述符不为-1说明已经打开，故异常退出需要关闭
    if(RTPSession->s32Sock >= 0)
    {
        close(RTPSession->s32Sock);
    }
    return -1;
}

//+------------------------------------------------------------------------------------------+
//|  函数名称：L_RTP_DestorySession
//|  功能描述：销毁一个RTP会话，主要是销毁对客户端的Socket端口。
//|  参数说明：L_STRUCT_RTPSESSION结构体指针
//|  返回值说明：成功返回0，失败返回-1。
//|  备注：
//+------------------------------------------------------------------------------------------+
int L_RTP_DestorySession(L_STRUCT_RTPSESSION* RTPSession)
{
    if(!RTPSession->flag_inited)
    {
        printf("The Struct has NOT been inited!\n");
        goto QUIT;
    }

    close(RTPSession->s32Sock);

    RTPSession->flag_inited = 0;
    RTPSession->pData       = NULL;
    RTPSession->DataLength  = 0;
    RTPSession->DataType    = RTP_NONE;
    return 0;

QUIT:
    return -1;
}

//+------------------------------------------------------------------------------------------+
//|  函数名称：L_RTP_Send_H264NALU
//|  功能描述：发送H264数据流。
//|  参数说明：L_STRUCT_RTPSESSION结构体指针
//|  返回值说明：成功返回0，失败返回-1。
//|  备注：用此函数发送的数据流不应包含起始码。
//+------------------------------------------------------------------------------------------+
static int L_RTP_Send_H264NALU(L_STRUCT_RTPSESSION* RTPSession)
{
    int ret = 0;
    unsigned char NALUByte;
    L_STRUCT_RTPHEADER *pRTPHeader;
    unsigned char *pRTPSendBuf;

    //****************************************
    //RTP首部是12个字节，通过对结构体的强制类型转换
    //将RTP首部的存放位置与申请的发送缓冲区共享。
    //****************************************
    pRTPSendBuf = (unsigned char *)calloc(RTP_H264_PACKAGE_LENGTH+60, sizeof(unsigned char));
    if(pRTPSendBuf == NULL)
    {
        printf("Calloc failed!\n");
        ret = -1;
        goto QUIT;
    }
    pRTPHeader = (L_STRUCT_RTPHEADER *)pRTPSendBuf;
    
    //初始化RTP首部公共部分
    pRTPHeader->u7Payload = RTP_HEAD_PAYLOAD_H264;
    pRTPHeader->u2Version = 2;
    pRTPHeader->u1Marker  = 0;
    pRTPHeader->u32SSrc   = RTPSession->u32SrcIP;

    //****************************************
    //时间戳计算
    //这里计算使用的是编码器生成H264码流的时候
    //码流包中携带的时间戳信息，为无符号64位整型值，
    //单位是us。
    //RTP码流中视频时间戳是基于90KHz的，故其单位为
    //1/90000，RTP首部中的时间戳要换算成该单位下
    //的变量，方法是（以下计算忽略了变量范围）：
    //RTPTimeStamp=H264TimeStamp*90000/1000000。
    //****************************************
    pRTPHeader->u32TimeStamp = htonl((unsigned long)(RTPSession->TimeStamp*9/100));

    //****************************************
    //由于RTP的定义时依照大端模式进行的，所以
    //所有有关RTP协议部分的参数都需要转换成
    //对应格式，这是NALU头需要单独提取的原因。
    //****************************************
    //提取NALU头
    NALUByte = *(RTPSession->pData);
    RTPSession->pData++;
    RTPSession->DataLength--;

    //****************************************
    //分片处理，对分片的说明见分片长度宏定义注释
    //分片和不分片，对NALU头的处理方式是不同的。
    //在不分片的情况下，一个RTP包格式是这样的：
    //RTP首部（12Bytes）+NALU（1Byte）+数据
    //在分片的时候，分片前端是这样的：
    //RTP首部（12Bytes）+FUA指示（1Byte）
    //+FUA头（1Byte）+数据+...
    //这时，NALU头会被分为两个部分分别存放在
    //FUA指示字节和FUA头中。
    //****************************************
    if(RTPSession->DataLength <= RTP_H264_PACKAGE_LENGTH)
    {
        L_STRUCT_RTP_NALUHEADER *pRTPNALUHeader;
        pRTPHeader->u1Marker  = 1;
        pRTPHeader->u16SeqNum = htons(RTPSession->SequenceNum++);

        //将NALU头写到RTP首部之后（占用1Byte）
        pRTPNALUHeader = (L_STRUCT_RTP_NALUHEADER *)(pRTPSendBuf+12);
        pRTPNALUHeader->u1F    = (NALUByte & 0x80) >> 7;
        pRTPNALUHeader->u2Nri  = (NALUByte & 0x60) >> 5;
        pRTPNALUHeader->u5Type = NALUByte & 0x1f;

        //拷贝数据流到缓冲区
        memcpy(pRTPSendBuf+13, RTPSession->pData, RTPSession->DataLength);

        //发送数据到目的地址
        if(sendto(RTPSession->s32Sock, pRTPSendBuf, RTPSession->DataLength+13, 0, (struct sockaddr *)&RTPSession->stDestAddr, sizeof(RTPSession->stDestAddr)) < 0)
        {
            printf("Socket send failed!\n");
            ret = -1;
            goto QUIT;
        }
    }
    else
    {
        L_STRUCT_RTP_FUA_INDICATOR *pRTPFUAIndicator;
        L_STRUCT_RTP_FUA_HEADER    *pRTPFUAHeader;
        int tmp_is_first   = 1;//用于指示分批发送的数据是否为第一批
        int tmp_sendlength = 0;//用于记录每次发送的数据长度

        //填充FUA指示字节
        pRTPFUAIndicator = (L_STRUCT_RTP_FUA_INDICATOR *)(pRTPSendBuf+12);
        pRTPFUAIndicator->u1F    = (NALUByte & 0x80) >> 7;
        pRTPFUAIndicator->u2Nri  = (NALUByte & 0x60) >> 5;
        pRTPFUAIndicator->u5Type = 28;

        //填充FUA头固定部分
        pRTPFUAHeader = (L_STRUCT_RTP_FUA_HEADER *)(pRTPSendBuf+13);
        pRTPFUAHeader->u1R    = 0;
        pRTPFUAHeader->u5Type = NALUByte & 0x1f;

        //分批发送数据
        while(RTPSession->DataLength>0)
        {
            //配置每包RTP首部
            pRTPHeader->u16SeqNum = htons(RTPSession->SequenceNum++);
            pRTPHeader->u1Marker  = (RTPSession->DataLength <= RTP_H264_PACKAGE_LENGTH)?1:0;

            //配置FU头
            pRTPFUAHeader->u1E = (RTPSession->DataLength<=RTP_H264_PACKAGE_LENGTH)?1:0;
            if(tmp_is_first == 1)
            {
                pRTPFUAHeader->u1S = 1;
                tmp_is_first = 0;
            }
            else
            {
                pRTPFUAHeader->u1S = 0;
            }
            
            //计算每次数据提取长度并存入缓冲
            tmp_sendlength = (RTPSession->DataLength <= RTP_H264_PACKAGE_LENGTH)?RTPSession->DataLength:RTP_H264_PACKAGE_LENGTH;
            memcpy(pRTPSendBuf+14, RTPSession->pData, tmp_sendlength);

            //计算发送数据长度并发送数据
            tmp_sendlength += 14;
            if(sendto(RTPSession->s32Sock, pRTPSendBuf, tmp_sendlength, 0, (struct sockaddr *)&RTPSession->stDestAddr, sizeof(RTPSession->stDestAddr)) < 0)
            {
                printf("Socket send failed!\n");
                ret = -1;
                goto QUIT;
            }
            RTPSession->pData += RTP_H264_PACKAGE_LENGTH;
            RTPSession->DataLength -= RTP_H264_PACKAGE_LENGTH;
        }
        //发送完成后复位数据指针和长度变量
        RTPSession->pData      = NULL;
        RTPSession->DataLength = 0;
    }

QUIT:
    if(pRTPSendBuf != NULL)
    {
        free((void *)pRTPSendBuf);
    }
    return ret;
}

//+------------------------------------------------------------------------------------------+
//|  函数名称：L_RTP_Send
//|  功能描述：通过RTP发送数据。
//|  参数说明：L_STRUCT_RTPSESSION结构体指针
//|  返回值说明：成功返回0，失败返回-1。
//|  备注：对于测试使用的海思平台编码的H264流，发送需要采取多包模式，每包一发，这是本程序的去
//|        起始码部分的编写方式决定的。
//+------------------------------------------------------------------------------------------+
int L_RTP_Send(L_STRUCT_RTPSESSION* RTPSession)
{
    if(RTPSession->DataType == RTP_NONE || RTPSession->pData == NULL || RTPSession->DataLength == 0)
    {
        printf("Structure is not effectively populated!\n");
        goto QUIT;
    }

    switch(RTPSession->DataType)
    {
    case RTP_H264:
        //****************************************
        //首先去掉数据流的起始码（00 00 00 01）
        //H264起始码占有4个字节，所以对于有起始码的数据
        //只需要使其指针指向的位置后移4位。
        //对应的数据长度减少4即可。
        //
        //然而需要注意的是，有可能不是所有的编码器都是
        //4个字节的起始码。
        //****************************************
        RTPSession->pData      = RTPSession->pData + 4;
        RTPSession->DataLength = RTPSession->DataLength - 4;
    
    case RTP_H264NALU:
        L_RTP_Send_H264NALU(RTPSession);
        break;
    case RTP_NONE:
    default:
        printf("There is no corresponding operation function for this type!\n");
        goto QUIT;
    }
    return 0;

QUIT:
    return -1;
}
